linux 键盘按键频率统计

·

按键频率(key frequency)

统计 Linux 下自己实际敲击的 按键频率(key frequency),从而为 键盘布局优化(如重映射高频键到更符合人体工学的位置)提供数据支持

常用的监控按键的工具

  • wev(wayland)
    • 通过wayland(libinput)读取eventX(evdev)
  • libinput list-devices
    • apt install libinput-tools
  • xev(x11)
    • 通过x11(xkb)读取eventX
  • xinput(x11)
  • evtest
    • apt install evtest
    • 直接读取eventX
  • showkey
    • 通过内核tty系统读物eventX
    • 只能在 VT(Ctrl+Alt+Fx)
    • 看的是 内核 tty keycode / scancode
  • uinput
    • 创建虚拟输入设备/dev/intput/eventY
    • /dev/uinput(虚拟设备注入点)
  • /dev/input/eventX
    • 内核输入子系统 (evdev)
  • 键盘映射系统级修改:修改内核键码映射(setkeycodes)或控制台映射(loadkeys)

使用 evtest + 自定义脚本(推荐,安全、底层、不依赖 GUI)

  • 所有数据仅保存在 ~/.local/share/keyfreq/,不记录键值内容(如不区分 a/A,只记录 KEY_A)。
  • 无法恢复原始文本,仅统计物理按键频率,不构成键盘记录器(keylogger)
sudo apt install evtest  # Debian/Ubuntu
sudo evtest

sudo apt install python3-pip
sudo usermod -aG input $USER
# 重新登录使组生效
pip3 install --user evdev
# 目录结构
# ~/.local/bin/keyfreqd
# ~/.config/systemd/user/keyfreqd.service
# ~/.local/share/keyfreq/  # 日志目录
#!/usr/bin/env python3
import sys
import struct
import os
from collections import Counter

# 从 evtest 或 /usr/include/linux/input-event-codes.h 获取 keycode 映射
KEY_NAMES = {
    1: 'ESC', 2: '1', 3: '2', 4: '3', 5: '4', 6: '5', 7: '6', 8: '7', 9: '8', 10: '9', 11: '0',
    12: 'MINUS', 13: 'EQUAL', 14: 'BACKSPACE',
    15: 'TAB', 16: 'Q', 17: 'W', 18: 'E', 19: 'R', 20: 'T', 21: 'Y', 22: 'U', 23: 'I', 24: 'O', 25: 'P',
    26: 'LEFTBRACE', 27: 'RIGHTBRACE', 28: 'ENTER',
    29: 'LEFTCTRL', 30: 'A', 31: 'S', 32: 'D', 33: 'F', 34: 'G', 35: 'H', 36: 'J', 37: 'K', 38: 'L',
    39: 'SEMICOLON', 40: 'APOSTROPHE', 41: 'GRAVE',
    42: 'LEFTSHIFT', 43: 'BACKSLASH', 44: 'Z', 45: 'X', 46: 'C', 47: 'V', 48: 'B', 49: 'N', 50: 'M',
    51: 'COMMA', 52: 'DOT', 53: 'SLASH', 54: 'RIGHTSHIFT',
    56: 'LEFTALT', 57: 'SPACE', 58: 'CAPSLOCK',
    97: 'RIGHTCTRL', 100: 'RIGHTALT',
    # 方向键等
    103: 'UP', 105: 'LEFT', 106: 'RIGHT', 108: 'DOWN',
}

def main(device_path):
    counter = Counter()
    with open(device_path, 'rb') as f:
        while True:
            data = f.read(24)  # input_event 结构体大小
            if not data:
                break
            tv_sec, tv_usec, type_, code, value = struct.unpack('llHHi', data)
            if type_ == 1 and value == 1:  # EV_KEY 且按下(非释放)
                key_name = KEY_NAMES.get(code, f'KEY_{code}')
                counter[key_name] += 1
                print(f"\r{len(counter)} keys recorded...", end='', flush=True)
    return counter

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: sudo python3 keyfreq.py /dev/input/eventX")
        sys.exit(1)
    device = sys.argv[1]
    print(f"Recording key presses from {device} (Press Ctrl+C to stop)...")
    try:
        counter = main(device)
    except KeyboardInterrupt:
        print("\nStopped.")
    # 输出排序结果
    print("\n=== Key Frequency ===")
    for key, count in counter.most_common():
        print(f"{key:12} : {count}")
#!/usr/bin/env python3
# ~/.local/bin/keyfreqd

import os
import json
import time
from datetime import datetime, date
from collections import defaultdict
from pathlib import Path

from evdev import InputDevice, categorize, ecodes, list_devices

# 配置
LOG_DIR = Path.home() / ".local/share/keyfreq"
LOG_DIR.mkdir(parents=True, exist_ok=True)

# 过滤出键盘设备(基于名称或 capability)
def is_keyboard(dev):
    if not dev.capabilities().get(ecodes.EV_KEY):
        return False
    name = dev.name.lower()
    # 常见键盘关键词
    keywords = ['keyboard', 'keypad', 'at keyboard', 'input device']
    return any(kw in name for kw in keywords) or 'kbd' in name

def main():
    # 获取所有输入设备
    devices = [InputDevice(fn) for fn in list_devices()]
    keyboards = [dev for dev in devices if is_keyboard(dev)]

    if not keyboards:
        print("No keyboard devices found. Check permissions (add user to 'input' group).", file=os.sys.stderr)
        return

    print(f"Monitoring {len(keyboards)} keyboard(s): {[d.name for d in keyboards]}")

    # 当天的计数器
    today = date.today()
    counter = defaultdict(int)

    try:
        while True:
            # 检查日期是否变更
            new_day = date.today()
            if new_day != today:
                # 保存旧数据
                save_day_log(today, counter)
                # 重置
                today = new_day
                counter = defaultdict(int)

            # 读取事件(非阻塞轮询)
            for dev in keyboards:
                try:
                    for event in dev.read():
                        if event.type == ecodes.EV_KEY:
                            key_event = categorize(event)
                            if hasattr(key_event, 'keycode'):
                                # 只记录按下(避免重复计数)
                                if event.value == 1:  # key press
                                    counter[key_event.keycode] += 1
                except BlockingIOError:
                    continue  # 无事件可读
                except OSError as e:
                    if e.errno == 19:  # 设备被拔出
                        keyboards = [d for d in keyboards if d != dev]
                        print(f"Device removed: {dev.name}", file=os.sys.stderr)
                    else:
                        raise

            time.sleep(0.01)  # 减少 CPU 占用

    except KeyboardInterrupt:
        save_day_log(today, counter)
        print("\nExiting and saving final log.")

def save_day_log(day: date, counter: dict):
    if not counter:
        return
    log_file = LOG_DIR / f"{day.isoformat()}.json"
    # 合并已有数据(以防重复运行)
    if log_file.exists():
        with open(log_file, 'r') as f:
            existing = json.load(f)
        for k, v in counter.items():
            existing[k] = existing.get(k, 0) + v
        counter = existing
    with open(log_file, 'w') as f:
        json.dump(dict(counter), f, indent=2, sort_keys=True)
    print(f"Saved {sum(counter.values())} keystrokes to {log_file}")

if __name__ == "__main__":
    main()
  • systemd 用户服务
cat ~/.config/systemd/user/keyfreqd.service
systemctl --user daemon-reload
systemctl --user enable --now keyfreqd.service

服务配置

[Unit]
Description=Keyboard Key Frequency Logger
After=graphical-session.target

[Service]
ExecStart=%h/.local/bin/keyfreqd
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=default.target

分析脚本

#!/usr/bin/env python3
# ~/.local/bin/keyfreq-analyze

import json
import sys
from pathlib import Path
from collections import defaultdict

LOG_DIR = Path.home() / ".local/share/keyfreq"

def main(days=7):
    total = defaultdict(int)
    count = 0
    for log in sorted(LOG_DIR.glob("*.json"))[-days:]:
        with open(log) as f:
            data = json.load(f)
            for k, v in data.items():
                total[k] += v
        count += 1

    if not total:
        print("No data found.")
        return

    print(f"Top 30 keys in last {count} day(s):")
    print("-" * 40)
    for key, freq in sorted(total.items(), key=lambda x: -x[1])[:30]:
        print(f"{key:15} : {freq:>8}")

if __name__ == "__main__":
    days = int(sys.argv[1]) if len(sys.argv) > 1 else 7
    main(days)